home *** CD-ROM | disk | FTP | other *** search
/ Skunkware 98 / Skunkware 98.iso / osr5 / sco / scripts / bills < prev    next >
Encoding:
AWK Script  |  1997-08-26  |  55.1 KB  |  1,614 lines

  1. #!/usr/local/bin/gawk -f
  2. # @(#) bills.gawk 3.2 97/07/25
  3. # 90/03/27 john h. dubois iii (john@armory.com)
  4. # 91/01/13 cleaned up the code, added features
  5. # 91/06/30 Changed field separator to be one or more tabs
  6. # 92/02/16 Added help option
  7. # 92/05/01 Converted to #!gawk script.
  8. #          Use gawk because it pays attention when we change ARGV[],
  9. #          and has strftime()
  10. # 94/04/23 Use .billrc
  11. # 95/03/21 Added rx options.  Made insensitive to case of payment type.
  12. #          Changed format of results.
  13. # 95/06/13 Added [Not] options.  Warn about dates in the future, wrong # of
  14. #          fields, and invalid payment values.
  15. #          Print total only if more than one payment type found.
  16. # 95/06/18 Added gGHk options.
  17. # 95/08/03 Let extra info be stored in type field.
  18. # 95/08/17 Added sorting, pPReEand options, Purc & $/Purc fields.
  19. # 96/01/20 Search for rcfile in $UHOME as well as $HOME.
  20. #          Make dollars part of values optional.
  21. # 96/01/21 Added AlMTw options.
  22. # 96/05/06 Search only for BILLS in the environment.
  23. # 96/05/27 Print whole line after error encountered.  Added y option.
  24. # 96/06/21 Added i option.
  25. # 97/07/25 Let # at the start of 2nd or 3rd field (in addition to 1st) make a
  26. #          line be treated as a comment, so sorting by date can work on it.
  27.  
  28. BEGIN {
  29.     Name = "bills"
  30.     rcFile = ".billrc"
  31.     Width = 19
  32.     Usage = "Usage: " Name \
  33.         " [-aAeGhHklnNMopPrRyY] [-g<tag>] [-T<tabstring>] [-t<type[,...]>]\n"\
  34.     "           "\
  35.     "  [-s<startdate>] [-d<enddate>] [-w<width>] [-E<mapfile>] [file ...]"
  36.     # pseudo -f arg is for BILLS rcfile option
  37.     ARGC = Opts(Name,Usage,"f:s:d:rot:NgGkneE:aRpPw<T:AhHi:lMxyY",0,
  38.     "~/" rcFile ":$UHOME/" rcFile,
  39. "BILLS,STARTDATE,ENDDATE,RUNNING,ORDER,TYPES,NOHEADER,TAG,EMPTYTAG,NOCHECK,"\
  40. "NAMETYPES,EXPENSETYPES,EXPENSEFILE,AMOUNTSORT,REVERSESORT,NUMSORT,AVGSORT,"\
  41. "WIDTH,TABSTRING",1,"")
  42.     if ("h" in Options) {
  43.     printf \
  44. "%s: report total and per-day expenditures for various types of payments.\n"\
  45. "%s\n"\
  46. "If no files are given, the file given in the environment variable BILLS is\n"\
  47. "used.  BILLS can also be set in the configuration file (see below) in this\n"\
  48. "form:\n"\
  49. "BILLS=filename\n"\
  50. "If no filenames are given and BILLS is not set, the standard input is used.\n"\
  51. "For each type of payment found, a line is printed giving statistics for\n"\
  52. "payments of that type: the total amount, the amount per day, week, month,\n"\
  53. "and year during the period from the first to last day in the input, and\n"\
  54. "the number of payments and the average value per payment.\n" \
  55. "The output is sorted by the payment type by default.\n"\
  56. "Payments may be grouped by one of three methods: payment-type, payee-name,\n"\
  57. "and expense-type.  By default, the payment type is used; this is the type\n"\
  58. "given as the third field of each payment line.  Alternately, the name of\n"\
  59. "the payee can be used as the payment type, with the name optionally mapped\n"\
  60. "through an expense-types file to a canonical name to allow for variations.\n"\
  61. "If the mapping file is used, names that are not found in it are preceded\n"\
  62. "by a '!'.  Finally, in the expense-types file each name can be mapped to\n"\
  63. "an expense type, so that names can be categorized as e.g. grocery,\n"\
  64. "gasoline, etc. expenses.  Names not found in the map are mapped to the\n"\
  65. "type \"misc\".\n"\
  66. "Options:\n"\
  67. "Some of the following options can also be set by assigning values to\n"\
  68. "variables in a configuration file named %s, which is searched for in the\n"\
  69. "invoking user's home directory and in the directory specified by the\n"\
  70. "environment variable UHOME, if it is set (if both files exist, values set\n"\
  71. "in the former take precedence).  Variables are assigned to with the\n"\
  72. "syntax:  varname=value  or in the case of flags, by simply putting the\n"\
  73. "indicated variable name in the file without a value.  Variable names are\n"\
  74. "given in parentheses in the option descriptions.\n",Name,Usage,rcFile
  75.     printf TabOpts(\
  76. "General options:\n"\
  77. "-h: Print this help.\n"\
  78. "-H: Print a description of the data and expense-types file formats.\n"\
  79. "-r: Print each line as it is read, preceded by the new sum for the type\n"\
  80. "    of payment the line is for.  (RUNNING)\n"\
  81. "-E<filename>: Set the name of the expense-types mapping file.  (MAPFILE)\n"\
  82. "Output sorting options (default: sort by payment type):\n"\
  83. "-a: Sort by amounts.  (AMOUNTSORT)\n"\
  84. "-p: Sort by number of payments.  (NUMSORT)\n"\
  85. "-P: Sort by average payment amount.  (AVGSORT)\n"\
  86. "-R: Display the sorted output in reverse order.  (REVERSESORT)\n"\
  87. "Record selection options (default: process all records):\n"\
  88. "-s<startdate>: Entries with dates before <startdate> will be ignored.\n"\
  89. "    <startdate> should be in the form [yy/]mm/dd.  (STARTDATE)\n"\
  90. "-d<enddate>: Entries with dates after <enddate> will be ignored.\n"\
  91. "    <enddate> should be in the form [yy/]mm/dd.  (ENDDATE)\n"\
  92. "-g<tag>: Process only lines that come after the tag line containing <tag>.\n"\
  93. "    (TAG)\n"\
  94. "-G: Process only lines that come after the tag line containing an empty\n"\
  95. "    tag.  (EMPTYTAG)\n"\
  96. "-t<type[,type,...]>: Process only records having a payment type given in\n"\
  97. "    the comma-separated list.  Types are not case sensitive.  (TYPES)\n"\
  98. "-i<payment-info-pattern>: Process only records having a payment-info field\n"\
  99. "    (the part of the payment type field that comes after the colon, if\n"\
  100. "    any) that matches <payment-info-pattern>, which may be a pattern in\n"\
  101. "    the style of egrep(C), implicitely anchored at the start and end.  If\n"\
  102. "    a null string or other pattern that would match an empty string is\n"\
  103. "    given with -i, in addition to matching records that have an empty\n"\
  104. "    payment-info field (nothing after the colon) it will match records\n"\
  105. "    having no payment-info field (no colon in the payment type field).\n"\
  106. "File checking options:\n"\
  107. "-o: Warn about entries that have dates that are out of sequence.  (ORDER)\n"\
  108. "-y: Print (only) all types found in the mapfile.\n"\
  109. "-Y: Like -y, except that types are printed in multiple columns.\n"\
  110. "-l: Print (only) all names found, sorted by length.\n"\
  111. "-M: Print (only) names not found in the mapfile.\n"\
  112. "-A: Print (only) aliases found in the types file that are not mapped.\n"\
  113. "-k: Skip some of the normal file sanity checks, to process large files\n"\
  114. "    slightly faster.  (NOCHECK)\n"\
  115. "Grouping options (default: group by payment type):\n"\
  116. "-n: Use the payee name as the grouping type.  If an expense-type mapping\n"\
  117. "    file is given it will be used to canonicalize the names.  Names not\n"\
  118. "    found in the mapfile will be prefixed with '!'.  (NAMETYPES)\n"\
  119. "-e: Use the expense type as the grouping type.  If no expense-type\n"\
  120. "    filename is given, the default filename "types" is used; it is\n"\
  121. "    expected to exist in the same directory that the first datafile is in.\n"\
  122. "    (EXPENSETYPES)\n"\
  123. "Output formatting options:\n"\
  124. "-N: Do not print any header or total lines.  (NOHEADER)\n"\
  125. "-w<width>: Set the maximum width of the Type field in the output (the\n"\
  126. "    default is %d characters).  A width of 0 causes the type field to\n"\
  127. "    never be truncated.  (WIDTH)\n"\
  128. "-T<tabstring>: Turn off constant-width Type field formatting; instead, the\n"\
  129. "    type field is separated from the next by <tabstring>.  (TABSTRING)\n",
  130.     "  "," -"),Width
  131.     ExitNow = 1
  132.     }
  133.     if ("H" in Options) {
  134.     printf \
  135. "Data file format:\n"\
  136. "Input lines are comment lines, payment lines, and tag lines.\n"\
  137. "Lines that have a # at the start of the first, second, or third field are\n"\
  138. "comment lines and are ignored.\n" \
  139. "Payment lines have this format:\n" \
  140. "date<tabs>amount<tabs>type[:<pay-info>]<tabs>name[<tabs>comment]\n" \
  141. "where date is in the form [year/]month/day (month should be numeric),\n"\
  142. "amount is a decimal number without a leading '$',\n"\
  143. "type is the type of payment (e.g., Cash, Check, etc.),\n"\
  144. "name is the name of the party the payment was made to,\n"\
  145. "and comment is anything (it is not used, other than being printed if the\n"\
  146. "r option is given).\n"\
  147. "If a year is not given the payment is taken to have occured in the\n"\
  148. "current year.  Payment types are not case sensitive.  The first occurance\n"\
  149. "of each payment type sets the capitalization that will be used in the\n"\
  150. "report.  A colon (:) and anything after it in the payment type are the\n"\
  151. "option pay-info field and are ignored by default.  This allows extra\n"\
  152. "information to be stored in the type field; for example, the month in\n"\
  153. "which the transaction appeared on a credit card bill.\n"\
  154. "Fields are separated by one or more tabs.\n"\
  155. "Examples:\n"\
  156. "95/04/05    1.49    Cash    Santa Cruz Hardware    Sanding paper\n"\
  157. "95/07/23    16.04    MC:08    British Petroleum\n"\
  158. "Tag lines have this format:\n"\
  159. "date tag [tag-value]\n"\
  160. "where date is the same as for a payment line, tag is the literal string\n"\
  161. "\"tag\", and tag-value is either empty (to use with the G option) or a tag\n"\
  162. "string to be given with the g option.  Tags are case sensitive.\n"\
  163. "Example:\n"\
  164. "95/05/16 tag StartNow\n"\
  165. "The input need not be sorted by date.  The order will affect the results\n"\
  166. "only when the tag options are used.\n"\
  167. "\n"\
  168. "Expense-types mapping file format:\n"\
  169. "Input lines are comment lines, expense-type lines, and alias lines.\n"\
  170. "Lines that start with # are comment lines and are ignored.\n"\
  171. "Expense-type lines have this format:\n"\
  172. "Name<tabs>Type\n"\
  173. "where Name is a name that may occur in a datafile and Type is an\n"\
  174. "expense-type.\n"\
  175. "Alias lines have this format:\n"\
  176. "Name=Canonical-name\n"\
  177. "where Name is a name that may occur in a datafile, and Canonical-name is\n"\
  178. "a name that is mapped to an expense-type elsewhere in the file.\n"\
  179. "The = character can only occur on an alias line, separating the two names.\n"\
  180. "Examples:\n"\
  181. "Logo's    books\n"\
  182. "Longs=Longs Drugs\n"\
  183. "Longs Drugs    drugstore\n"
  184.  
  185.     ExitNow = 1
  186.     }
  187.     if (ExitNow)
  188.     exit(0)
  189.     if ((Err = ExclusiveOptions("gG",Options)) != "") {
  190.     printf "Error: %s\n",Err
  191.     Err = 1
  192.     exit(1)
  193.     }
  194.     Debug = "x" in Options
  195.  
  196.     if (ARGC < 2 && "f" in Options) {
  197.     ARGV[1] = Options["f"]
  198.     ARGC = 2
  199.     }
  200.  
  201.     FS = "\t+"
  202.     if ("s" in Options)
  203.     start = makedate(Options["s"])
  204.     else
  205.     start = 0
  206.     if ("d" in Options)
  207.     end = makedate(Options["d"])
  208.     else
  209.     end = 2000000000
  210.     Running = "r" in Options
  211.     Order = "o" in Options
  212.     AmountSort = "a" in Options
  213.     NumSort = "p" in Options
  214.     AvgSort = "P" in Options
  215.     ReverseSort = "R" in Options
  216.     NoHeader = "N" in Options
  217.     # Turn n option on if [AlMy] is given so that mapfile will be read.
  218.     NameTypes = (unMappedOnly = "M" in Options) ||
  219.     (typesOnly = ("y" in Options || "Y" in Options)) ||
  220.     (namesOnly = "l" in Options) || (badAliasesOnly = "A" in Options)
  221.     Fast = "k" in Options
  222.     if ("T" in Options) {
  223.     tabString = Options["T"]
  224.     Width = 0
  225.     }
  226.     if ("g" in Options)
  227.     Tag = Options["g"]
  228.     if ("i" in Options)
  229.     infoPat = "^(" Options["i"] ")$"
  230.     TagWait = "g" in Options || "G" in Options
  231.     if ("w" in Options)
  232.     Width = Options["w"]
  233.     if (TypesGiven = ("t" in Options))
  234.     MakeSet(Types,tolower(Options["t"]),",")
  235.     first = 10000000
  236.     last = 0
  237. #    "date +%y" | getline year
  238.     year = strftime("%y")
  239.     year *= 10000
  240.     split("0 31 59 90 120 151 181 212 243 273 304 334",months," ")
  241.     CurDate = strftime("%y%m%d")
  242.     if ((NameTypes = NameTypes || "n" in Options) ||
  243.     (ExpTypes = "e" in Options)) {
  244.     # Read expense types file
  245.     if ("E" in Options)
  246.         ExpTypeFile = Options["E"]
  247.     else if (ARGC > 1) {
  248.         ExpTypeFile = ARGV[1]
  249.         sub("[^/]*$","types",ExpTypeFile)
  250.     }
  251.     else    # If reading stdin, expect tags in current dir
  252.         ExpTypeFile = "types"
  253.     if (Debug)
  254.         printf "Reading mapfile %s\n",ExpTypeFile > "/dev/stderr"
  255.     # If we cannot read the mapping file, and either it was given
  256.     # explicitly or we have to have it for the e option, give up.
  257.     if (ReadTypeFile(CanonMap,TypeMap,ExpTypeFile) == -1) {
  258.         if ("E" in Options || ExpTypes) {
  259.         printf \
  260.         "Error reading expense-type mapping file %s:\n%s.  Exiting.\n",
  261.         ExpTypeFile,ERRNO
  262.         Err = 1
  263.         exit 1
  264.         }
  265.         if (Debug)
  266.         printf \
  267.         "Error reading expense-type mapping file %s:\n%s.\n",
  268.         ExpTypeFile,ERRNO
  269.     }
  270.     else
  271.         GoodMap = 1
  272.     if (Debug)
  273.         printf "Done reading mapfile.\n" > "/dev/stderr"
  274.     if (typesOnly) {
  275.         for (name in TypeMap)
  276.         Types[TypeMap[name]]
  277.         n = qsortByArbIndex(Types,k,0)
  278.         if ("y" in Options)
  279.         for (i = 1; i <= n; i++)
  280.             print k[i]
  281.         else {
  282.         HeadTailInit(-1)
  283.         PrintDown(k,2,COLUMNS-1)
  284.         }
  285.     }
  286.     if (typesOnly || badAliasesOnly) {
  287.         ExitNow = 1
  288.         exit 0
  289.     }
  290.     }
  291.     else
  292.     TypeTypes = 1    # Collate by payment-type field
  293. }
  294.  
  295. ### Start of record-processing blocks
  296.  
  297. $1 ~ /^#/ || $2 ~ /^#/ || $3 ~ /^#/ {
  298.     next
  299. }
  300.  
  301. $2 == "tag" {
  302.     if (TagWait) {
  303.     if (!Fast)
  304.         date = GetDate()
  305.     if ($3 == Tag) {
  306.         TagWait = 0
  307.         if (Debug)
  308.         printf "Found tag \"%s\" on input line %d\n",Tag,NR > \
  309.         "/dev/stderr"
  310.     }
  311.     }
  312.     next
  313. }
  314.  
  315. {
  316.     if (TagWait)
  317.     next
  318.     if (!Fast) {
  319.     if (NF != 4 && NF != 5) {
  320.         printf "Error: %d fields in record %d; should be 4 or 5\n>> %s\n",
  321.         NF,NR,$0 > "/dev/stderr"
  322.         next
  323.     }
  324.     # A $ value must be: optional minus sign, optional dollars part,
  325.     # optional cents part, with at least one digit (so that just a minus
  326.     # sign will not suffice).
  327.     if ($2 !~ /^-?[0-9]*(\.[0-9]+)?$/ || $2 !~ /[0-9]/) {
  328.         printf "Error: invalid payment value in record %d: %s\n>> %s\n",
  329.         NR,$2,$0 > "/dev/stderr"
  330.         next
  331.     }
  332.     }
  333.     date = GetDate()
  334.  
  335.     if (TypeTypes) {
  336.     Type = $3
  337.     sub(":.*","",Type)
  338.     }
  339.     else {
  340.     lName = tolower($4)
  341.     if (NameTypes) {
  342.         if (GoodMap) {
  343.         if (lName in CanonMap)
  344.             Type = CanonMap[lName]
  345.         else {
  346.             Type = CanonMap[lName] = $4
  347.             unMapped[lName]
  348.         }
  349.         }
  350.         else
  351.         Type = $4
  352.     }
  353.     else     # ExpTypes
  354.         if (lName in CanonMap)
  355.         Type = TypeMap[tolower(CanonMap[lName])]
  356.         else
  357.         Type = "misc"
  358.     }
  359.     LType = tolower(Type)
  360.     if (infoPat != "") {
  361.     payInfo = $3
  362.     if (!sub("[^:]*:","",payInfo))
  363.         payInfo = ""
  364.     if (payInfo !~ infoPat)
  365.         next
  366.     }
  367.  
  368.     # Selection by date and type occurs here
  369.     if (date >= start && date <= end && (!TypesGiven || LType in Types)) {
  370.     if (date < first)
  371.         first = date
  372.     if (date > last)
  373.         last = date
  374.     # The first instance of each type sets its capitalization
  375.     if (!(LType in Capitalized))
  376.         Capitalized[LType] = Type
  377.     sum[LType] += $2
  378.     Count[LType]++
  379.     if (Running) {
  380.         if (Debug)
  381.         printf "[%s] ",unmakedate(date)
  382.         printf "%8.2f %s\n",sum[LType],$0
  383.     }
  384.     }
  385. }
  386.  
  387. ### End of record-processing blocks
  388.  
  389. function GetDate(  date) {
  390.     if ((date = makedate($1)) == -1) {
  391.     printf "Bad date in record number %d: %s\n",NR,$1 > "/dev/stderr"
  392.     next
  393.     }
  394.     if (date > CurDate)
  395.     printf "Warning: Record %d has a date in the future: %s\n",
  396.     NR,unmakedate(date) > "/dev/stderr"
  397.     if (Order) {
  398.     if (date < PrevDate)
  399.         printf "Warning: Record %d has an earlier date than record %d.\n",
  400.         NR,NR-1 > "/dev/stderr"
  401.     # Set date to this date line even if it was out of order,
  402.     # because the previous one might be the real culprit.
  403.     PrevDate = date
  404.     }
  405.     return date
  406. }
  407.  
  408. function Fmt(Amount) {
  409.     return sprintf("%.2f",Amount)
  410. }
  411.  
  412. function PrintLine(Type,Len,  Amount,PerDay) {
  413.     Amount = sum[Type]
  414.     PerDay = Amount/datediff
  415.     printf(Format,substr((Type in unMapped ? "!" : "")Capitalized[Type],1,Len),
  416.     sprintf("%.1f",Amount/Total*100), Fmt(Amount),Fmt(PerDay),Fmt(PerDay * 7),
  417.     Fmt(PerDay * 30),Fmt(PerDay * 365), Count[Type], 
  418.     Count[Type] ? Fmt(Amount/Count[Type]) : "-")
  419.     return Amount
  420. }
  421.  
  422. END {
  423.     if (Err)
  424.     exit Err
  425.     if (ExitNow)
  426.     exit(0)
  427.     if (first == 10000000) {
  428.     print "0 records processed." > "/dev/stderr"
  429.     exit(0)
  430.     }
  431.     firstday = date2days(first)
  432.     lastday = date2days(last)
  433.     datediff = lastday - firstday
  434.     # Start with name field with of 5 because that is how long "Total" is
  435.     NameWidth = 5
  436.     # Total for each type is in sum[type]
  437.     for (Type in sum) {
  438.     if ((l = length(Type)) > NameWidth)
  439.         NameWidth = l
  440.     if (Type != TotalType) {
  441.         GotType = 1
  442.         Total += sum[Type]
  443.         TotalCount += Count[Type]
  444.         if (Debug && !sum[Type])
  445.         printf "0 sum for type %s\n",Type > "/dev/stderr"
  446.     }
  447.     }
  448.     if (!GotType) {
  449.     print "No types found?!" > "/dev/stderr"
  450.     exit(1)
  451.     }
  452.     if (namesOnly || unMappedOnly) {
  453.     if (namesOnly) {
  454.         for (lName in Capitalized)
  455.         nameLengths[lName] = length(lName)
  456.         Num = qsortArbIndByValue(nameLengths,k)
  457.     }
  458.     else
  459.         Num = qsortByArbIndex(unMapped,k)
  460.     for (i = 1; i <= Num; i++)
  461.         print Capitalized[k[i]]
  462.     exit 0
  463.     }
  464.     if (!Total) {
  465.     print "Sum of amounts is 0."
  466.     exit(0)
  467.     }
  468.     FW = "8"
  469.     if (Width)
  470.     NameWidth = min(NameWidth,Width)
  471.     if (tabString == "")
  472.     Format = "%-" NameWidth "s %5s %" FW "s %" FW-2 "s %" FW-1 "s %" FW-1 \
  473.     "s %" FW "s %4s %7s\n"
  474.     else
  475.     Format = "%s" tabString "%5s %" FW "s %" FW-2 "s %" FW-1 "s %" FW-1 \
  476.     "s %" FW "s %4s %7s\n"
  477.     if (!NoHeader) {
  478.     printf("Period: %s to %s (%d days).\n",unmakedate(first),
  479.     unmakedate(last),datediff)
  480.     printf Format,
  481.     "Type","%","$Total","$/Day","$/Week","$/Month","$/Year","Num","Avg$"
  482.     }
  483.     if (AmountSort)
  484.     Num = qsortArbIndByValue(sum,k)
  485.     else if (NumSort)
  486.     Num = qsortArbIndByValue(Count,k)
  487.     else if (AvgSort) {
  488.     for (lName in sum)
  489.         Ind[lName] = sum[lName] / Count[lName]
  490.     Num = qsortArbIndByValue(Ind,k)
  491.     }
  492.     else
  493.     Num = qsortByArbIndex(sum,k)
  494.     if (ReverseSort)
  495.     for (i = Num; i >= 1; i--)
  496.         PrintLine(k[i],NameWidth)
  497.     else
  498.     for (i = 1; i <= Num; i++)
  499.         PrintLine(k[i],NameWidth)
  500.     if (NumElem(sum) > 1 && !NoHeader) {
  501.     TotalType = "_total"
  502.     Capitalized[TotalType] = "Total"
  503.     sum[TotalType] = Total
  504.     Count[TotalType] = TotalCount
  505.     PrintLine(TotalType,NameWidth)
  506.     }
  507. }
  508.  
  509. # Read mapping file TypeFile.
  510. # CanonMap[] maps every name to a canonical name.
  511. # TypeMap[] maps every canonical name to an expense type.
  512. # Array indexes are moved to lower case; values in CanonMap are mixed case.
  513. function ReadTypeFile(CanonMap,TypeMap,TypeFile,
  514. LineNum,l,Line,i,ret,ErrCount) {
  515.     FS = "\t+|="
  516.     while ((ret = (getline < TypeFile)) == 1) {
  517.     LineNum++
  518.     if ($0 ~ /^#/)    # comment
  519.         continue
  520.     if (NF != 2 || $1 ~ /^ *$/ || $2 ~ /^ *$/) {
  521.         printf \
  522.     "Error on line %d of type file %s:\nLine does not have two fields:\n%s\n",
  523.         LineNum,TypeFile,$0 > "/dev/stderr"
  524.         continue
  525.     }
  526.     l = tolower($1)
  527.     if ($0 ~ /\t/) {
  528.         TypeMap[l] = $2
  529.         CanonMap[l] = $1
  530.     }
  531.     else
  532.         CanonMap[l] = $2
  533.     Line[l] = LineNum
  534.     }
  535.     for (i in CanonMap) {
  536.     What = CanonMap[i]
  537.     if (!((l = tolower(What)) in TypeMap)) {
  538.         if (badAliasesOnly) {
  539.         if (!(l in badAliases)) {
  540.             print What
  541.             badAliases[l]
  542.         }
  543.         }
  544.         else
  545.         printf \
  546.         "Error on line %d of type file %s:\n"\
  547.         "%s is aliased to %s, which is not mapped.\n",
  548.         Line[i],TypeFile,i,What > "/dev/stderr"
  549.         delete CanonMap[i]
  550.         ErrCount++
  551.     }
  552.     }
  553.     if (ret)
  554.     return -1
  555.     else
  556.     return ErrCount
  557. }
  558.  
  559. # convert month/day or year/month/day date to yymmdd date
  560. # uses global "year" var if year not given
  561. function makedate(InDate,Elements,d,date) {
  562.     Elements = split(InDate,d,"/")
  563.     date = d[1] * 100 + d[2]
  564.     if (Elements == 2)
  565.     date += year
  566.     else if (Elements == 3)
  567.     date = date * 100 + d[3]
  568.     else
  569.     return -1
  570.     return date
  571. }
  572.  
  573. # convert yymmdd date to yy/mm/dd date
  574. function unmakedate(Date) {
  575.     return substr(Date,1,2) "/" substr(Date,3,2) "/" substr(Date,5,2)
  576. }
  577.  
  578. function date2days(date) {
  579.     return(date % 100 + months[0 + substr(date,3,2)] + substr(date,1,2) * 365)
  580. }
  581.  
  582. # MakeSet: make a set from a list.
  583. # An index with the name of each element of the list
  584. # is created in the given array.
  585. # Input variables: 
  586. # Elements is a string containing the list of elements.
  587. # Sep is the character that separates the elements of the list.
  588. # Output variables:
  589. # Set is the array.
  590. # Return value: the number of elements added to the set.
  591. function MakeSet(Set,Elements,Sep,  i,Num,Names) {
  592.     Num = split(Elements,Names,Sep)
  593.     for (i = 1; i <= Num; i++)
  594.     Set[Names[i]]
  595.     return Num
  596. }
  597.  
  598. # Returns the number of elements in set Set
  599. function NumElem(Set,  elem,Num) {
  600.     for (elem in Set)
  601.     Num++
  602.     return Num
  603. }
  604.  
  605. ### Begin min,max,In routines
  606.  
  607. function min(a,b) {
  608.     if (a < b)
  609.     return a
  610.     else
  611.     return b
  612. }
  613.  
  614. function max(a,b) {
  615.     if (a > b)
  616.     return a
  617.     else
  618.     return b
  619. }
  620.  
  621. function In(Val,Min,Max) {
  622.     return (Min <= Val && Val <= Max)
  623. }
  624.  
  625. # Return (in Ind) the indices of the elements with the smallest value in A.
  626. # The smallest value is returned as the function value.
  627. # If there are no elements in A, null is returned.
  628. function arrMin(A,Ind,  i,min) {
  629.     for (i in A)
  630.     if (min == "" || A[i] < min) {
  631.         DeleteAll(Ind)
  632.         min = A[i]
  633.         Ind[i]
  634.     }
  635.     else if (A[i] == min)
  636.         Ind[i]
  637.     return min
  638. }
  639.  
  640. ### End min,max,In routines
  641. ### Begin qsort routines
  642.  
  643. # Arr[] is an array of values with arbitrary indices.
  644. # k[] is returned with numeric indices 1..n.
  645. # The values in k[] are the indices of Arr[],
  646. # ordered so that if Arr[] is stepped through
  647. # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped
  648. # through in order of the values of its elements.
  649. # The return value is the number of elements in the arrays (n).
  650. function qsortArbIndByValue(Arr,k,  ArrInd,ElNum) {
  651.     ElNum = 0
  652.     for (ArrInd in Arr)
  653.     k[++ElNum] = ArrInd
  654.     qsortSegment(Arr,k,1,ElNum)
  655.     return ElNum
  656. }
  657.  
  658. # Sort a segment of an array.
  659. # Arr[] contains data with arbitrary indices.
  660. # k[] has indices 1..nelem, with the indices of arr[] as values.
  661. # This function sorts the elements of arr that are pointed to by
  662. # k[start..end], swapping the values of elements of k[] so that
  663. # when this function returns arr[k[start..end]] will be in order.
  664. function qsortSegment(Arr,k,start,end,  left,right,sepval,tmp,tmpe,tmps) {
  665.     # handle two-element case explicitly for a tiny speedup
  666.     if ((end - start) == 1) {
  667.     if (Arr[tmps = k[start]] > Arr[tmpe = k[end]]) {
  668.         k[start] = tmpe
  669.         k[end] = tmps
  670.     }
  671.     return
  672.     }
  673.     # Make sure comparisons act on these as numbers
  674.     left = start+0
  675.     right = end+0
  676.     sepval = Arr[k[int((left + right) / 2)]]
  677.     # Make every element <= sepval be to the left of every element > sepval
  678.     while (left < right) {
  679.     while (Arr[k[left]] < sepval)
  680.         left++
  681.     while (Arr[k[right]] > sepval)
  682.         right--
  683.     if (left < right) {
  684.         tmp = k[left]
  685.         k[left++] = k[right]
  686.         k[right--] = tmp
  687.     }
  688.     }
  689.     if (left == right)
  690.     if (Arr[k[left]] < sepval)
  691.         left++
  692.     else
  693.         right--
  694.     if (start < right)
  695.     qsortSegment(Arr,k,start,right)
  696.     if (left < end)
  697.     qsortSegment(Arr,k,left,end)
  698. }
  699.  
  700. # Arr[] is an array of values with arbitrary indices.
  701. # k[] is returned with numeric indices 1..n.
  702. # The values in k are the indices of Arr[],
  703. # ordered so that if Arr[] is stepped through
  704. # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped
  705. # through in order of the values of its indices.
  706. # The return value is the number of elements in the arrays (n).
  707. # If the indexes are numeric, Numeric should be true, so that they can be
  708. # compared as such rather than as strings.  Numeric indexes do not have to be
  709. # contiguous.
  710. function qsortByArbIndex(Arr,k,Numeric,  ArrInd,ElNum) {
  711.     ElNum = 0
  712.     if (Numeric)
  713.     # Indexes do not preserve numeric type, so must be forced
  714.     for (ArrInd in Arr)
  715.         k[++ElNum] = ArrInd+0
  716.     else
  717.     for (ArrInd in Arr)
  718.         k[++ElNum] = ArrInd
  719.     qsortNumIndByValue(k,1,ElNum)
  720.     return ElNum
  721. }
  722.  
  723. # Arr is an array of elements with contiguous numeric indexes to be sorted
  724. # by value.
  725. # start and end are the starting and ending indexes of the range to be sorted.
  726. function qsortNumIndByValue(Arr,start,end,  left,right,sepval,tmp,tmpe,tmps) {
  727.     # handle two-element case explicitly for a tiny speedup
  728.     if ((start - end) == 1) {
  729.     if ((tmps = Arr[start]) > (tmpe = Arr[end])) {
  730.         Arr[start] = tmpe
  731.         Arr[end] = tmps
  732.     }
  733.     return
  734.     }
  735.     left = start+0
  736.     right = end+0
  737.     sepval = Arr[int((left + right) / 2)]
  738.     while (left < right) {
  739.     while (Arr[left] < sepval)
  740.         left++
  741.     while (Arr[right] > sepval)
  742.         right--
  743.     if (left <= right) {
  744.         tmp = Arr[left]
  745.         Arr[left++] = Arr[right]
  746.         Arr[right--] = tmp
  747.     }
  748.     }
  749.     if (start < right)
  750.     qsortNumIndByValue(Arr,start,right)
  751.     if (left < end)
  752.     qsortNumIndByValue(Arr,left,end)
  753. }
  754.  
  755. ### End qsort routines
  756.  
  757. ### Start of ProcArgs library
  758. # @(#) ProcArgs 1.11 96/12/08
  759. # 92/02/29 john h. dubois iii (john@armory.com)
  760. # 93/07/18 Added "#" arg type
  761. # 93/09/26 Do not count -h against MinArgs
  762. # 94/01/01 Stop scanning at first non-option arg.  Added ">" option type.
  763. #          Removed meaning of "+" or "-" by itself.
  764. # 94/03/08 Added & option and *()< option types.
  765. # 94/04/02 Added NoRCopt to Opts()
  766. # 94/06/11 Mark numeric variables as such.
  767. # 94/07/08 Opts(): Do not require any args if h option is given.
  768. # 95/01/22 Record options given more than once.  Record option num in argv.
  769. # 95/06/08 Added ExclusiveOptions().
  770. # 96/01/20 Let rcfiles be a colon-separated list of filenames.
  771. #          Expand $VARNAME at the start of its filenames.
  772. #          Let varname=0 and -option- turn off an option.
  773. # 96/05/05 Changed meaning of 7th arg to Opts; now can specify exactly how many
  774. #          of the vars should be searched for in the environment.
  775. #          Check for duplicate rcfiles.
  776. # 96/05/13 Return more specific error values.  Note: ProcArgs() and InitOpts()
  777. #          now return various negatives values on error, not just -1, and
  778. #          Opts() may set Err to various positive values, not just 1.
  779. #          Added AllowUnrecOpt.
  780. # 96/05/23 Check type given for & option
  781. # 96/06/15 Re-port to awk
  782. # 96/10/01 Moved file-reading code into ReadConfFile(), so that it can be
  783. #          used by other functions.
  784. # 96/10/15 Added OptChars
  785. # 96/11/01 Added exOpts arg to Opts()
  786. # 96/11/16 Added ; type
  787. # 96/12/08 Added Opt2Set() & Opt2Sets()
  788. # 96/12/27 Added CmdLineOpt()
  789.  
  790. # optlist is a string which contains all of the possible command line options.
  791. # A character followed by certain characters indicates that the option takes
  792. # an argument, with type as follows:
  793. # :    String argument
  794. # ;    Non-empty string argument
  795. # *    Floating point argument
  796. # (    Non-negative floating point argument
  797. # )    Positive floating point argument
  798. # #    Integer argument
  799. # <    Non-negative integer argument
  800. # >    Positive integer argument
  801. # The only difference the type of argument makes is in the runtime argument
  802. # error checking that is done.
  803.  
  804. # The & option is a special case used to get numeric options without the
  805. # user having to give an option character.  It is shorthand for [-+.0-9].
  806. # If & is included in optlist and an option string that begins with one of
  807. # these characters is seen, the value given to "&" will include the first
  808. # char of the option.  & must be followed by a type character other than ":"
  809. # or ";".
  810. # Note that if e.g. &> is given, an option of -.5 will produce an error.
  811.  
  812. # Strings in argv[] which begin with "-" or "+" are taken to be
  813. # strings of options, except that a string which consists solely of "-"
  814. # or "+" is taken to be a non-option string; like other non-option strings,
  815. # it stops the scanning of argv and is left in argv[].
  816. # An argument of "--" or "++" also stops the scanning of argv[] but is removed.
  817. # If an option takes an argument, the argument may either immediately
  818. # follow it or be given separately.
  819. # "-" and "+" options are treated the same.  "+" is allowed because most awks
  820. # take any -options to be arguments to themselves.  gawk 2.15 was enhanced to
  821. # stop scanning when it encounters an unrecognized option, though until 2.15.5
  822. # this feature had a flaw that caused problems in some cases.  See the OptChars
  823. # parameter to explicitly set the option-specifier characters.
  824.  
  825. # If an option that does not take an argument is given,
  826. # an index with its name is created in Options and its value is set to the
  827. # number of times it occurs in argv[].
  828.  
  829. # If an option that does take an argument is given, an index with its name is
  830. # created in Options and its value is set to the value of the argument given
  831. # for it, and Options[option-name,"count"] is (initially) set to the 1.
  832. # If an option that takes an argument is given more than once,
  833. # Options[option-name,"count"] is incremented, and the value is assigned to
  834. # the index (option-name,instance) where instance is 2 for the second occurance
  835. # of the option, etc.
  836. # In other words, the first time an option with a value is encountered, the
  837. # value is assigned to an index consisting only of its name; for any further
  838. # occurances of the option, the value index has an extra (count) dimension.
  839.  
  840. # The sequence number for each option found in argv[] is stored in
  841. # Options[option-name,"num",instance], where instance is 1 for the first
  842. # occurance of the option, etc.  The sequence number starts at 1 and is
  843. # incremented for each option, both those that have a value and those that
  844. # do not.  Options set from a config file have a value of 0 assigned to this.
  845.  
  846. # Options and their arguments are deleted from argv.
  847. # Note that this means that there may be gaps left in the indices of argv[].
  848. # If compress is nonzero, argv[] is packed by moving its elements so that
  849. # they have contiguous integer indices starting with 0.
  850. # Option processing will stop with the first unrecognized option, just as
  851. # though -- was given except that unlike -- the unrecognized option will not be
  852. # removed from ARGV[].  Normally, an error value is returned in this case.
  853. # If AllowUnrecOpt is true, it is not an error for an unrecognized option to
  854. # be found, so the number of remaining arguments is returned instead.
  855. # If OptChars is not a null string, it is the set of characters that indicate
  856. # that an argument is an option string if the string begins with one of the
  857. # characters.  A string consisting solely of two of the same option-indicator
  858. # characters stops the scanning of argv[].  The default is "-+".
  859. # argv[0] is not examined.
  860. # The number of arguments left in argc is returned.
  861. # If an error occurs, the global string OptErr is set to an error message
  862. # and a negative value is returned.
  863. # Current error values:
  864. # -1: option that required an argument did not get it.
  865. # -2: argument of incorrect type supplied for an option.
  866. # -3: unrecognized (invalid) option.
  867. function ProcArgs(argc,argv,OptList,Options,compress,AllowUnrecOpt,OptChars,
  868. ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue,specGiven,
  869. NeedNextOpt,GotValue,OptionNum,Escape,dest,src,count,c,OptTerm,OptCharSet)
  870. {
  871. # ArgNum is the index of the argument being processed.
  872. # ArgsLeft is the number of arguments left in argv.
  873. # Arg is the argument being processed.
  874. # ArgLen is the length of the argument being processed.
  875. # ArgInd is the position of the character in Arg being processed.
  876. # Option is the character in Arg being processed.
  877. # Pos is the position in OptList of the option being processed.
  878. # NumOpt is true if a numeric option may be given.
  879.     ArgsLeft = argc
  880.     NumOpt = index(OptList,"&")
  881.     OptionNum = 0
  882.     if (OptChars == "")
  883.     OptChars = "-+"
  884.     while (OptChars != "") {
  885.     c = substr(OptChars,1,1)
  886.     OptChars = substr(OptChars,2)
  887.     OptCharSet[c]
  888.     OptTerm[c c]
  889.     }
  890.     for (ArgNum = 1; ArgNum < argc; ArgNum++) {
  891.     Arg = argv[ArgNum]
  892.     if (length(Arg) < 2 || !((specGiven = substr(Arg,1,1)) in OptCharSet))
  893.         break    # Not an option; quit
  894.     if (Arg in OptTerm) {
  895.         delete argv[ArgNum]
  896.         ArgsLeft--
  897.         break
  898.     }
  899.     ArgLen = length(Arg)
  900.     for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) {
  901.         Option = substr(Arg,ArgInd,1)
  902.         if (NumOpt && Option ~ /[-+.0-9]/) {
  903.         # If this option is a numeric option, make its flag be & and
  904.         # its option string flag position be the position of & in
  905.         # the option string.
  906.         Option = "&"
  907.         Pos = NumOpt
  908.         # Prefix Arg with a char so that ArgInd will point to the
  909.         # first char of the numeric option.
  910.         Arg = "&" Arg
  911.         ArgLen++
  912.         }
  913.         # Find position of flag in option string, to get its type (if any).
  914.         # Disallow & as literal flag.
  915.         else if (!(Pos = index(OptList,Option)) || Option == "&") {
  916.         if (AllowUnrecOpt) {
  917.             Escape = 1
  918.             break
  919.         }
  920.         else {
  921.             OptErr = "Invalid option: " specGiven Option
  922.             return -3
  923.         }
  924.         }
  925.  
  926.         # Find what the value of the option will be if it takes one.
  927.         # NeedNextOpt is true if the option specifier is the last char of
  928.         # this arg, which means that if the option requires a value it is
  929.         # the next arg.
  930.         if (NeedNextOpt = (ArgInd >= ArgLen)) { # Value is the next arg
  931.         if (GotValue = ArgNum + 1 < argc)
  932.             Value = argv[ArgNum+1]
  933.         }
  934.         else {    # Value is included with option
  935.         Value = substr(Arg,ArgInd + 1)
  936.         GotValue = 1
  937.         }
  938.  
  939.         if (HadValue = AssignVal(Option,Value,Options,
  940.         substr(OptList,Pos + 1,1),GotValue,"",++OptionNum,!NeedNextOpt,
  941.         specGiven)) {
  942.         if (HadValue < 0)    # error occured
  943.             return HadValue
  944.         if (HadValue == 2)
  945.             ArgInd++    # Account for the single-char value we used.
  946.         else {
  947.             if (NeedNextOpt) {    # option took next arg as value
  948.             delete argv[++ArgNum]
  949.             ArgsLeft--
  950.             }
  951.             break    # This option has been used up
  952.         }
  953.         }
  954.     }
  955.     if (Escape)
  956.         break
  957.     # Do not delete arg until after processing of it, so that if it is not
  958.     # recognized it can be left in ARGV[].
  959.     delete argv[ArgNum]
  960.     ArgsLeft--
  961.     }
  962.     if (compress != 0) {
  963.     dest = 1
  964.     src = argc - ArgsLeft + 1
  965.     for (count = ArgsLeft - 1; count; count--) {
  966.         ARGV[dest] = ARGV[src]
  967.         dest++
  968.         src++
  969.     }
  970.     }
  971.     return ArgsLeft
  972. }
  973.  
  974. # Assignment to values in Options[] occurs only in this function.
  975. # Option: Option specifier character.
  976. # Value: Value to be assigned to option, if it takes a value.
  977. # Options[]: Options array to return values in.
  978. # ArgType: Argument type specifier character.
  979. # GotValue: Whether any value is available to be assigned to this option.
  980. # Name: Name of option being processed.
  981. # OptionNum: Number of this option (starting with 1) if set in argv[],
  982. #     or 0 if it was given in a config file or in the environment.
  983. # SingleOpt: true if the value (if any) that is available for this option was
  984. #     given as part of the same command line arg as the option.  Used only for
  985. #     options from the command line.
  986. # specGiven is the option specifier character use, if any (e.g. - or +),
  987. # for use in error messages.
  988. # Global variables: OptErr
  989. # Return value: negative value on error, 0 if option did not require an
  990. # argument, 1 if it did & used the whole arg, 2 if it required just one char of
  991. # the arg.
  992. # Current error values:
  993. # -1: Option that required an argument did not get it.
  994. # -2: Value of incorrect type supplied for option.
  995. # -3: Bad type given for option &
  996. function AssignVal(Option,Value,Options,ArgType,GotValue,Name,OptionNum,
  997. SingleOpt,specGiven,  UsedValue,Err,NumTypes) {
  998.     # If option takes a value...    [
  999.     NumTypes = "*()#<>]"
  1000.     if (Option == "&" && ArgType !~ "[" NumTypes) {    # ]
  1001.     OptErr = "Bad type given for & option"
  1002.     return -3
  1003.     }
  1004.  
  1005.     if (UsedValue = (ArgType ~ "[:;" NumTypes)) {    # ]
  1006.     if (!GotValue) {
  1007.         if (Name != "")
  1008.         OptErr = "Variable requires a value -- " Name
  1009.         else
  1010.         OptErr = "option requires an argument -- " Option
  1011.         return -1
  1012.     }
  1013.     if ((Err = CheckType(ArgType,Value,Option,Name,specGiven)) != "") {
  1014.         OptErr = Err
  1015.         return -2
  1016.     }
  1017.     # Mark this as a numeric variable; will be propogated to Options[] val.
  1018.     if (ArgType != ":" && ArgType != ";")
  1019.         Value += 0
  1020.     if ((Instance = ++Options[Option,"count"]) > 1)
  1021.         Options[Option,Instance] = Value
  1022.     else
  1023.         Options[Option] = Value
  1024.     }
  1025.     # If this is an environ or rcfile assignment & it was given a value...
  1026.     else if (!OptionNum && Value != "") {
  1027.     UsedValue = 1
  1028.     # If the value is "0" or "-" and this is the first instance of it,
  1029.     # do not set Options[Option]; this allows an assignment in an rcfile to
  1030.     # turn off an option (for the simple "Option in Options" test) in such
  1031.     # a way that it cannot be turned on in a later file.
  1032.     if (!(Option in Options) && (Value == "0" || Value == "-"))
  1033.         Instance = 1
  1034.     else
  1035.         Instance = ++Options[Option]
  1036.     # Save the value even though this is a flag
  1037.     Options[Option,Instance] = Value
  1038.     }
  1039.     # If this is a command line flag and has a - following it in the same arg,
  1040.     # it is being turned off.
  1041.     else if (OptionNum && SingleOpt && substr(Value,1,1) == "-") {
  1042.     UsedValue = 2
  1043.     if (Option in Options)
  1044.         Instance = ++Options[Option]
  1045.     else
  1046.         Instance = 1
  1047.     Options[Option,Instance]
  1048.     }
  1049.     # If this is a flag assignment without a value, increment the count for the
  1050.     # flag unless it was turned off.  The indicator for a flag being turned off
  1051.     # is that the flag index has not been set in Options[] but it has an
  1052.     # instance count.
  1053.     else if (Option in Options || !((Option,1) in Options))
  1054.     # Increment number of times this flag seen; will inc null value to 1
  1055.     Instance = ++Options[Option]
  1056.     Options[Option,"num",Instance] = OptionNum
  1057.     return UsedValue
  1058. }
  1059.  
  1060. # Option is the option letter
  1061. # Value is the value being assigned
  1062. # Name is the var name of the option, if any
  1063. # ArgType is one of:
  1064. # :    String argument
  1065. # ;    Non-null string argument
  1066. # *    Floating point argument
  1067. # (    Non-negative floating point argument
  1068. # )    Positive floating point argument
  1069. # #    Integer argument
  1070. # <    Non-negative integer argument
  1071. # >    Positive integer argument
  1072. # specGiven is the option specifier character use, if any (e.g. - or +),
  1073. # for use in error messages.
  1074. # Returns null on success, err string on error
  1075. function CheckType(ArgType,Value,Option,Name,specGiven,  Err,ErrStr) {
  1076.     if (ArgType == ":")
  1077.     return ""
  1078.     if (ArgType == ";") {
  1079.     if (Value == "")
  1080.         Err = "must be a non-empty string"
  1081.     }
  1082.     # A number begins with optional + or -, and is followed by a string of
  1083.     # digits or a decimal with digits before it, after it, or both
  1084.     else if (Value !~ /^[-+]?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)$/)
  1085.     Err = "must be a number"
  1086.     else if (ArgType ~ "[#<>]" && Value ~ /\./)
  1087.     Err = "may not include a fraction"
  1088.     else if (ArgType ~ "[()<>]" && Value < 0)
  1089.     Err = "may not be negative"
  1090.     # (
  1091.     else if (ArgType ~ "[)>]" && Value == 0)
  1092.     Err = "must be a positive number"
  1093.     if (Err != "") {
  1094.     ErrStr = "Bad value \"" Value "\".  Value assigned to "
  1095.     if (Name != "")
  1096.         return ErrStr "variable " substr(Name,1,1) " " Err
  1097.     else {
  1098.         if (Option == "&")
  1099.         Option = Value
  1100.         return ErrStr "option " specGiven substr(Option,1,1) " " Err
  1101.     }
  1102.     }
  1103.     else
  1104.     return ""
  1105. }
  1106.  
  1107. # Note: only the above functions are needed by ProcArgs.
  1108. # The rest of these functions call ProcArgs() and also do other
  1109. # option-processing stuff.
  1110.  
  1111. # Opts: Process command line arguments.
  1112. # Opts processes command line arguments using ProcArgs()
  1113. # and checks for errors.  If an error occurs, a message is printed
  1114. # and the program is exited.
  1115. #
  1116. # Input variables:
  1117. # Name is the name of the program, for error messages.
  1118. # Usage is a usage message, for error messages.
  1119. # OptList the option description string, as used by ProcArgs().
  1120. # MinArgs is the minimum number of non-option arguments that this
  1121. # program should have, non including ARGV[0] and +h.
  1122. # If the program does not require any non-option arguments,
  1123. # MinArgs should be omitted or given as 0.
  1124. # rcFiles, if given, is a colon-seprated list of filenames to read for
  1125. # variable initialization.  If a filename begins with ~/, the ~ is replaced
  1126. # by the value of the environment variable HOME.  If a filename begins with
  1127. # $, the part from the character after the $ up until (but not including)
  1128. # the first character not in [a-zA-Z0-9_] will be searched for in the
  1129. # environment; if found its value will be substituted, if not the filename will
  1130. # be discarded.
  1131. # rcfiles are read in the order given.
  1132. # Values given in them will not override values given on the command line,
  1133. # and values given in later files will not override those set in earlier
  1134. # files, because AssignVal() will store each with a different instance index.
  1135. # The first instance of each variable, either on the command line or in an
  1136. # rcfile, will be stored with no instance index, and this is the value
  1137. # normally used by programs that call this function.
  1138. # VarNames is a comma-separated list of variable names to map to options,
  1139. # in the same order as the options are given in OptList.
  1140. # If EnvSearch is given and nonzero, the first EnvSearch variables will also be
  1141. # searched for in the environment.  If set to -1, all values will be searched
  1142. # for in the environment.  Values given in the environment will override
  1143. # those given in the rcfiles but not those given on the command line.
  1144. # NoRCopt, if given, is an additional letter option that if given on the
  1145. # command line prevents the rcfiles from being read.
  1146. # See ProcArgs() for a description of AllowUnRecOpt and optChars, and
  1147. # ExclusiveOptions() for a description of exOpts.
  1148. # Special options:
  1149. # If x is made an option and is given, some debugging info is output.
  1150. # h is assumed to be the help option.
  1151.  
  1152. # Global variables:
  1153. # The command line arguments are taken from ARGV[].
  1154. # The arguments that are option specifiers and values are removed from
  1155. # ARGV[], leaving only ARGV[0] and the non-option arguments.
  1156. # The number of elements in ARGV[] should be in ARGC.
  1157. # After processing, ARGC is set to the number of elements left in ARGV[].
  1158. # The option values are put in Options[].
  1159. # On error, Err is set to a positive integer value so it can be checked for in
  1160. # an END block.
  1161. # Return value: The number of elements left in ARGV is returned.
  1162. # Must keep OptErr global since it may be set by InitOpts().
  1163. function Opts(Name,Usage,OptList,MinArgs,rcFiles,VarNames,EnvSearch,NoRCopt,
  1164. AllowUnrecOpt,optChars,exOpts,  ArgsLeft,e) {
  1165.     if (MinArgs == "")
  1166.     MinArgs = 0
  1167.     ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1,AllowUnrecOpt,
  1168.     optChars)
  1169.     if (ArgsLeft < (MinArgs+1) && !("h" in Options)) {
  1170.     if (ArgsLeft >= 0) {
  1171.         OptErr = "Not enough arguments"
  1172.         Err = 4
  1173.     }
  1174.     else
  1175.         Err = -ArgsLeft
  1176.     printf "%s: %s.\nUse -h for help.\n%s\n",
  1177.     Name,OptErr,Usage > "/dev/stderr"
  1178.     exit 1
  1179.     }
  1180.     if (rcFiles != "" && (NoRCopt == "" || !(NoRCopt in Options)) &&
  1181.     (e = InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch)) < 0)
  1182.     {
  1183.     print Name ": " OptErr ".\nUse -h for help." > "/dev/stderr"
  1184.     Err = -e
  1185.     exit 1
  1186.     }
  1187.     if ((exOpts != "") && ((OptErr = ExclusiveOptions(exOpts,Options)) != ""))
  1188.     {
  1189.     printf "%s: Error: %s\n",Name,OptErr > "/dev/stderr"
  1190.     Err = 1
  1191.     exit 1
  1192.     }
  1193.     return ArgsLeft
  1194. }
  1195.  
  1196. # ReadConfFile(): Read a file containing var/value assignments, in the form
  1197. # <variable-name><assignment-char><value>.
  1198. # Whitespace (spaces and tabs) around a variable (leading whitespace on the
  1199. # line and whitespace between the variable name and the assignment character) 
  1200. # is stripped.  Lines that do not contain an assignment operator or which
  1201. # contain a null variable name are ignored, other than possibly being noted in
  1202. # the return value.  If more than one assignment is made to a variable, the
  1203. # first assignment is used.
  1204. # Input variables:
  1205. # File is the file to read.
  1206. # Comment is the line-comment character.  If it is found as the first non-
  1207. #     whitespace character on a line, the line is ignored.
  1208. # Assign is the assignment string.  The first instance of Assign on a line
  1209. #     separates the variable name from its value.
  1210. # If StripWhite is true, whitespace around the value (whitespace between the
  1211. #     assignment char and trailing whitespace on the line) is stripped.
  1212. # VarPat is a pattern that variable names must match.  
  1213. #     Example: "^[a-zA-Z][a-zA-Z0-9]+$"
  1214. # If FlagsOK is true, variables are allowed to be "set" by being put alone on
  1215. #     a line; no assignment operator is needed.  These variables are set in
  1216. #     the output array with a null value.  Lines containing nothing but
  1217. #     whitespace are still ignored.
  1218. # Output variables:
  1219. # Values[] contains the assignments, with the indexes being the variable names
  1220. #     and the values being the assigned values.
  1221. # Lines[] contains the line number that each variable occured on.  A flag set
  1222. #     is record by giving it an index in Lines[] but not in Values[].
  1223. # Return value:
  1224. # If any errors occur, a string consisting of descriptions of the errors
  1225. # separated by newlines is returned.  In no case will the string start with a
  1226. # numeric value.  If no errors occur,  the number of lines read is returned.
  1227. function ReadConfigFile(Values,Lines,File,Comment,Assign,StripWhite,VarPat,
  1228. FlagsOK,
  1229. Line,Status,Errs,AssignLen,LineNum,Var,Val) {
  1230.     if (Comment != "")
  1231.     Comment = "^" Comment
  1232.     AssignLen = length(Assign)
  1233.     if (VarPat == "")
  1234.     VarPat = "."    # null varname not allowed
  1235.     while ((Status = (getline Line < File)) == 1) {
  1236.     LineNum++
  1237.     sub("^[ \t]+","",Line)
  1238.     if (Line == "")        # blank line
  1239.         continue
  1240.     if (Comment != "" && Line ~ Comment)
  1241.         continue
  1242.     if (Pos = index(Line,Assign)) {
  1243.         Var = substr(Line,1,Pos-1)
  1244.         Val = substr(Line,Pos+AssignLen)
  1245.         if (StripWhite) {
  1246.         sub("^[ \t]+","",Val)
  1247.         sub("[ \t]+$","",Val)
  1248.         }
  1249.     }
  1250.     else {
  1251.         Var = Line    # If no value, var is entire line
  1252.         Val = ""
  1253.     }
  1254.     if (!FlagsOK && Val == "") {
  1255.         Errs = Errs \
  1256.         sprintf("\nBad assignment on line %d of file %s: %s",
  1257.         LineNum,File,Line)
  1258.         continue
  1259.     }
  1260.     sub("[ \t]+$","",Var)
  1261.     if (Var !~ VarPat) {
  1262.         Errs = Errs sprintf("\nBad variable name on line %d of file %s: %s",
  1263.         LineNum,File,Var)
  1264.         continue
  1265.     }
  1266.     if (!(Var in Lines)) {
  1267.         Lines[Var] = LineNum
  1268.         if (Pos)
  1269.         Values[Var] = Val
  1270.     }
  1271.     }
  1272.     if (Status)
  1273.     Errs = Errs "\nCould not read file " File
  1274.     close(File)
  1275.     return Errs == "" ? LineNum : substr(Errs,2)    # Skip first newline
  1276. }
  1277.  
  1278. # Variables:
  1279. # Data is stored in Options[].
  1280. # rcFiles, OptList, VarNames, and EnvSearch are as as described for Opts().
  1281. # Global vars:
  1282. # Sets OptErr.  Uses ENVIRON[].
  1283. # If anything is read from any of the rcfiles, sets READ_RCFILE to 1.
  1284. function InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch,
  1285. Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret,i,rcFile,
  1286. fNames,numrcFiles,filesRead,Err,Values,retStr) {
  1287.     split("",filesRead,"")    # make awk know this is an array
  1288.     NumVars = split(VarNames,Vars,",")
  1289.     TypesInd = Ret = 0
  1290.     if (EnvSearch == -1)
  1291.     EnvSearch = NumVars
  1292.     for (i = 1; i <= NumVars; i++) {
  1293.     Var = Vars[i]
  1294.     CharOpt = substr(OptList,++TypesInd,1)
  1295.     if (CharOpt ~ "^[:;*()#<>&]$")
  1296.         CharOpt = substr(OptList,++TypesInd,1)
  1297.     Map[Var] = CharOpt
  1298.     Types[Var] = Type = substr(OptList,TypesInd+1,1)
  1299.     # Do not overwrite entries from environment
  1300.     if (i <= EnvSearch && Var in ENVIRON &&
  1301.     (Err = AssignVal(CharOpt,ENVIRON[Var],Options,Type,1,Var,0)) < 0)
  1302.         return Err
  1303.     }
  1304.  
  1305.     numrcFiles = split(rcFiles,fNames,":")
  1306.     for (i = 1; i <= numrcFiles; i++) {
  1307.     rcFile = fNames[i]
  1308.     if (rcFile ~ "^~/")
  1309.         rcFile = ENVIRON["HOME"] substr(rcFile,2)
  1310.     else if (rcFile ~ /^\$/) {
  1311.         rcFile = substr(rcFile,2)
  1312.         match(rcFile,"^[a-zA-Z0-9_]*")
  1313.         envvar = substr(rcFile,1,RLENGTH)
  1314.         if (envvar in ENVIRON)
  1315.         rcFile = ENVIRON[envvar] substr(rcFile,RLENGTH+1)
  1316.         else
  1317.         continue
  1318.     }
  1319.     if (rcFile in filesRead)
  1320.         continue
  1321.     # rcfiles are liable to be given more than once, e.g. UHOME and HOME
  1322.     # may be the same
  1323.     filesRead[rcFile]
  1324.     if ("x" in Options)
  1325.         printf "Reading configuration file %s\n",rcFile > "/dev/stderr"
  1326.     retStr = ReadConfigFile(Values,Lines,rcFile,"#","=",0,"",1)
  1327.     if (retStr > 0)
  1328.         READ_RCFILE = 1
  1329.     else if (ret != "") {
  1330.         OptErr = retStr
  1331.         Ret = -1
  1332.     }
  1333.     for (Var in Lines)
  1334.         if (Var in Map) {
  1335.         if ((Err = AssignVal(Map[Var],
  1336.         Var in Values ? Values[Var] : "",Options,Types[Var],
  1337.         Var in Values,Var,0)) < 0)
  1338.             return Err
  1339.         }
  1340.         else {
  1341.         OptErr = sprintf(\
  1342.         "Unknown var \"%s\" assigned to on line %d\nof file %s",Var,
  1343.         Lines[Var],rcFile)
  1344.         Ret = -1
  1345.         }
  1346.     }
  1347.  
  1348.     if ("x" in Options)
  1349.     for (Var in Map)
  1350.         if (Map[Var] in Options)
  1351.         printf "(%s) %s=%s\n",Map[Var],Var,Options[Map[Var]] > \
  1352.         "/dev/stderr"
  1353.         else
  1354.         printf "(%s) %s not set\n",Map[Var],Var > "/dev/stderr"
  1355.     return Ret
  1356. }
  1357.  
  1358. # OptSets is a semicolon-separated list of sets of option sets.
  1359. # Within a list of option sets, the option sets are separated by commas.  For
  1360. # each set of sets, if any option in one of the sets is in Options[] AND any
  1361. # option in one of the other sets is in Options[], an error string is returned.
  1362. # If no conflicts are found, nothing is returned.
  1363. # Example: if OptSets = "ab,def,g;i,j", an error will be returned due to
  1364. # the exclusions presented by the first set of sets (ab,def,g) if:
  1365. # (a or b is in Options[]) AND (d, e, or f is in Options[]) OR
  1366. # (a or b is in Options[]) AND (g is in Options) OR
  1367. # (d, e, or f is in Options[]) AND (g is in Options)
  1368. # An error will be returned due to the exclusions presented by the second set
  1369. # of sets (i,j) if: (i is in Options[]) AND (j is in Options[]).
  1370. # todo: make options given on command line unset options given in config file
  1371. # todo: that they conflict with.
  1372. function ExclusiveOptions(OptSets,Options,
  1373. Sets,SetSet,NumSets,Pos1,Pos2,Len,s1,s2,c1,c2,ErrStr,L1,L2,SetSets,NumSetSets,
  1374. SetNum,OSetNum) {
  1375.     NumSetSets = split(OptSets,SetSets,";")
  1376.     # For each set of sets...
  1377.     for (SetSet = 1; SetSet <= NumSetSets; SetSet++) {
  1378.     # NumSets is the number of sets in this set of sets.
  1379.     NumSets = split(SetSets[SetSet],Sets,",")
  1380.     # For each set in a set of sets except the last...
  1381.     for (SetNum = 1; SetNum < NumSets; SetNum++) {
  1382.         s1 = Sets[SetNum]
  1383.         L1 = length(s1)
  1384.         for (Pos1 = 1; Pos1 <= L1; Pos1++)
  1385.         # If any of the options in this set was given, check whether
  1386.         # any of the options in the other sets was given.  Only check
  1387.         # later sets since earlier sets will have already been checked
  1388.         # against this set.
  1389.         if ((c1 = substr(s1,Pos1,1)) in Options)
  1390.             for (OSetNum = SetNum+1; OSetNum <= NumSets; OSetNum++) {
  1391.             s2 = Sets[OSetNum]
  1392.             L2 = length(s2)
  1393.             for (Pos2 = 1; Pos2 <= L2; Pos2++)
  1394.                 if ((c2 = substr(s2,Pos2,1)) in Options)
  1395.                 ErrStr = ErrStr "\n"\
  1396.                 sprintf("Cannot give both %s and %s options.",
  1397.                 c1,c2)
  1398.             }
  1399.     }
  1400.     }
  1401.     if (ErrStr != "")
  1402.     return substr(ErrStr,2)
  1403.     return ""
  1404. }
  1405.  
  1406. # The value of each instance of option Opt that occurs in Options[] is made an
  1407. # index of Set[].
  1408. # The return value is the number of instances of Opt in Options.
  1409. function Opt2Set(Options,Opt,Set,  count) {
  1410.     if (!(Opt in Options))
  1411.     return 0
  1412.     Set[Options[Opt]]
  1413.     count = Options[Opt,"count"]
  1414.     for (; count > 1; count--)
  1415.     Set[Options[Opt,count]]
  1416.     return count
  1417. }
  1418.  
  1419. # The value of each instance of option Opt that occurs in Options[] that
  1420. # begins with "!" is made an index of nSet[] (with the ! stripped from it).
  1421. # Other values are made indexes of Set[].
  1422. # The return value is the number of instances of Opt in Options.
  1423. function Opt2Sets(Options,Opt,Set,nSet,  count,aSet,ret) {
  1424.     ret = Opt2Set(Options,Opt,aSet)
  1425.     for (value in aSet)
  1426.     if (substr(value,1,1) == "!")
  1427.         nSet[substr(value,2)]
  1428.     else
  1429.         Set[value]
  1430.     return ret
  1431. }
  1432.  
  1433. # Returns true if option Opt was given on the command line.
  1434. function CmdLineOpt(Options,Opt,  i) {
  1435.     for (i = 1; (Opt,"num",i) in Options; i++)
  1436.     if (Options[Opt,"num",i] != 0)
  1437.         return 1
  1438.     return 0
  1439. }
  1440. ### End of ProcArgs library
  1441.  
  1442. # In multiline string OptDesc, insert string Tab at the start of every line
  1443. # that begins with any of the characters in TabChars.
  1444. # If the first character in Tab also occurs in TabChars, make sure it is the
  1445. # last character in TabChars to avoid double indenting.
  1446. function TabOpts(OptDesc,Tab,TabChars,  i,len,c) {
  1447.     len = length(TabChars)
  1448.     for (i = 1; i <= len; i++) {
  1449.     c = substr(TabChars,i,1)
  1450.     sub("^" c,Tab c,OptDesc)
  1451.     gsub("\n" c,"\n" Tab c,OptDesc)
  1452.     }
  1453.     return OptDesc
  1454. }
  1455.  
  1456. ### Begin head-tail routines
  1457.  
  1458. # @(#) HeadTail.awk 96/05/09
  1459. # 95/04/28 Added tail routines.
  1460. # 96/05/09 Added all args to HeadTailInit()
  1461.  
  1462. # Turn on screen-bounded printing.
  1463. # Current implementation sets global vars LINES, COLUMNS, LINEGAP, and COLGAP.
  1464. # Sets the number of screen lines and rows to Lines and Rows.
  1465. # If -1 is passed for either, turns off bounding in that dimension.
  1466. # If either is not set or 0 is passed for it, its value is taken from the
  1467. # environment, or if not set there, from terminfo, or if not set there, from
  1468. # the defaults (24 and 80).
  1469. # By default, the other functions in this library leave a "grace space" of
  1470. # 1 column and 1 line.  If LineGap or ColGap is passed and is a non-negative
  1471. # value, the line gap is set to it.
  1472. function HeadTailInit(Lines,Cols,LineGap,ColGap,  Cmd) {
  1473.     # tput will use values in environment, but we want to avoid running
  1474.     # it if possible.
  1475.     if (Cols > 0)
  1476.     COLUMNS = Cols
  1477.     else if (!Cols)
  1478.     if ("COLUMNS" in ENVIRON)
  1479.         COLUMNS = ENVIRON["COLUMNS"]
  1480.     else {
  1481.         Cmd = "exec tput cols"
  1482.         Cmd | getline COLUMNS
  1483.         close(Cmd)
  1484.         if (COLUMNS == "")
  1485.         COLUMNS = 80
  1486.     }
  1487.     if (Lines > 0)
  1488.     LINES = Lines
  1489.     else if (!Lines)
  1490.     if ("LINES" in ENVIRON)
  1491.         LINES = ENVIRON["LINES"]
  1492.     else {
  1493.         Cmd = "exec tput lines"
  1494.         Cmd | getline LINES
  1495.         close(Cmd)
  1496.         if (LINES == "")
  1497.         LINES = 24
  1498.     }
  1499.     LINEGAP = (LineGap != "" && LineGap >= 0) ? LineGap : 1
  1500.     COLGAP = (ColGap != "" && ColGap >= 0) ? ColGap : 1
  1501. }
  1502.  
  1503. # Do screen-bound printing.  
  1504. # If LINES  is >0, the last LINES-LINEGAP lines are kept in a circular buffer.  
  1505. # When TailFlush() is called, they are printed.
  1506. # If LINES = 0, all lines are printed immediately.
  1507. # If COLUMNS is >0, truncates Line to COLUMNS-COLGAP characters before printing
  1508. # it.
  1509. # Global vars: uses LINES & COLUMNS; sets/uses TailPtr;
  1510. # saves lines in TailLines[] from 1..LINES-LINEGAP
  1511. # Embedded newlines split the line into multiple lines; trailing newlines are
  1512. # stripped.  Tabs are expanded to spaces.
  1513. function TailPrint(Line) {
  1514.     if (!LINES)
  1515.     print Line
  1516.     else {
  1517.     if (++TailPtr > (LINES-LINEGAP))
  1518.         TailPtr = 1
  1519.     TailLines[TailPtr] = Line
  1520.     }
  1521. }
  1522.  
  1523. function TailFlush(  NumPrinted,Lines,Line,i,Buffer,PrintLines) {
  1524.     if (!LINES)
  1525.     return
  1526.     NumPrinted = 0
  1527.     PrintLines = LINES-LINEGAP
  1528.     # Since lines may contain multiple lines, we must create a buffer to be
  1529.     # printed by reading line buffer backwards.
  1530.     # Stop when we have copied enough lines, or if we wrap around to the end
  1531.     # and find that the entire line buffer was not used.
  1532.     while (NumPrinted < PrintLines && TailPtr in TailLines) {
  1533.     # Split line into individual lines, then process them last to first
  1534.     Num = split(TailLines[TailPtr],Lines,"\n")
  1535.     for (i = Num; i >= 1; i--) {
  1536.         Line = Lines[i]
  1537.         if (i == Num && Line == "")    # discard trailing newline
  1538.         continue
  1539.         # Put this line at the front of the print buffer
  1540.         if (COLUMNS)
  1541.         Buffer = substr(TabEx(Line),1,COLUMNS - COLGAP) "\n" Buffer
  1542.         else
  1543.         Buffer = Line "\n" Buffer
  1544.         if (++NumPrinted == PrintLines)
  1545.         break
  1546.     }
  1547.     if (!--TailPtr)    # Wrap pointer if neccessary
  1548.         TailPtr = PrintLines
  1549.     }
  1550.     printf "%s",Buffer
  1551. }
  1552.  
  1553. # Do screen-bound printing.  
  1554. # If LINES >0, returns 0 when LINES-LINEGAP lines have been printed by
  1555. # HeadPrint().  Otherwise returns 1.
  1556. # If COLUMNS is >0, truncates Line to COLUMNS-COLGAP characters before printing
  1557. # it.
  1558. # Global vars: uses LINES, COLUMNS, LINEGAP, COLGAP; sets/uses LinesPrinted.
  1559. # Line should not include newlines.
  1560. function HeadPrint(Line) {
  1561.     # Check first, in case some calls of this function to not check return
  1562.     # value, and in case LINES is 1.
  1563.     if (LINES && LinesPrinted >= (LINES-LINEGAP))
  1564.     return 0
  1565.     if (COLUMNS)
  1566.     print substr(Line,1,COLUMNS - COLGAP)
  1567.     else
  1568.     print Line
  1569.     if (LINES && ++LinesPrinted >= (LINES-LINEGAP))
  1570.     return 0
  1571.     return 1
  1572. }
  1573.  
  1574. function ColPrint(Line) {
  1575.     if (COLUMNS)
  1576.     print substr(Line,1,COLUMNS - COLGAP)
  1577.     else
  1578.     print Line
  1579.     return 1
  1580. }
  1581.  
  1582. ### End head-tail routines
  1583.  
  1584. # Print the elements in Data[1..n] in multiple columns across the screen,
  1585. # printing from the top to the bottom of the first column, then top to bottom
  1586. # of second column, etc.
  1587. # Gutter is the number of spaces to print between columns.
  1588. # Width is the screen width to use.
  1589. # As many columns as will fit are printed across.
  1590. function PrintDown(Data,Gutter,Width,  l,Cols,Rows,len,i,Ind,Row) {
  1591.     # Find max string length.
  1592.     l = 0
  1593.     for (i = 1; i in Data; i++)
  1594.     if ((len = length(Data[i])) > l)
  1595.         l = len
  1596.     i--
  1597.     if (!l)
  1598.     return
  1599.     l += Gutter
  1600.     Cols = int((Width+Gutter)/l)
  1601.     if (!Cols)
  1602.     Cols = 1
  1603.     Rows = int(i/Cols+Cols-1)
  1604.     for (Row = 1; Row <= Rows; Row++) {
  1605.     Ind = Row 
  1606.     for (Col = 1; Col <= Cols; Col++) {
  1607.         if (Ind in Data)
  1608.         printf("%-*s",Col == Cols ? 0 : l,Data[Ind])
  1609.         Ind += Rows
  1610.     }
  1611.     print ""
  1612.     }
  1613. }
  1614.